跳到主要内容

Go 的 log 库

Go 标准库提供了一个 日志库 log

快速使用

log 默认输出到标准错误(stderr),每条日志前会自动加上日期和时间。如果日志不是以换行符结尾的,那么 log 会自动加上换行符。即每条日志会在新行中输出。

log 提供了三组函数:

  • Print/Printf/Println:正常输出日志;
  • Panic/Panicf/Panicln:输出日志后,以拼装好的字符串为参数调用panic;
  • Fatal/Fatalf/Fatalln:输出日志后,调用os.Exit(1)退出程序。

使用例:

package main

import (
"log"
)

type User struct {
Name string
Age int
}

func main() {
u := User{
Name: "dj",
Age: 18,
}

log.Printf("%s login, age:%d", u.Name, u.Age)
log.Fatalf("Danger! hacker %s login", u.Name) // 直接异常结束
log.Panicf("Oh, system error when %s login", u.Name) // 抛出 panic 异常
}

打印效果:

2021/10/26 21:07:08 dj login, age:18
2021/10/26 21:07:08 Danger! hacker dj login
exit status 1

因为 Fatalf 已经结束了所以看不到 Panicf 的效果,下面打印 Panicf 的返回效果

panic: Oh, system error when dj login

goroutine 1 [running]:
log.Panicf({0x10933c0, 0x2}, {0xc00007bf40, 0xc00007bf70, 0xfe52b9})
C:/Program Files/Go/src/log/log.go:361 +0x67
main.main()
C:/Users/33204/Desktop/Go/strpc/main.go:19 +0xcb
exit status 2

定制打印格式

添加前缀

调用 log.SetPrefix 为每条日志文本前增加一个前缀。例如,在上面的程序中设置Login:前缀:

func main() {
u := User{
Name: "dj",
Age: 18,
}

log.SetPrefix("Login: ")
log.Printf("%s login, age:%d", u.Name, u.Age)
}

调用 log.Prefix 可以获取当前设置的前缀。

打印效果:

Login: 2021/10/26 21:12:01 dj login, age:18

添加额外信息 Flag

设置选项可在每条输出的文本前增加一些额外信息,如日期时间、文件名等。

// src/log/log.go
const (
Ldate = 1 << iota // 输出当地时区的日期,如 2021/10/26;
Ltime // 输出当地时区的时间,如 21:12:01;
Lmicroseconds // 输出的时间精确到微秒,设置了该选项就不用设置Ltime了。如11:45:45.123123;
Llongfile // 输出长文件名+行号,含包名,如github.com/darjun/go-daily-lib/log/flag/main.go:50;
Lshortfile // 输出短文件名+行号,不含包名,如main.go:50;
LUTC // 如果设置了Ldate或Ltime,将输出 UTC 时间,而非当地时区。
)

调用 log.SetFlag 设置选项,可以一次设置多个:

func main() {
u := User{
Name: "dj",
Age: 18,
}
// 默认是 Ldate | Ltime
log.SetFlags(log.Lshortfile | log.Ldate | log.Lmicroseconds)

log.Printf("%s login, age:%d", u.Name, u.Age)
}

打印结果

2021/10/26 21:14:53.221404 main.go:19: dj login, age:18

自定义 Log

标准 Logger 实现

log 库为我们定义了一个默认的 Logger,名为 std,意为标准日志。直接调用的 log 库的方法,其内部是调用 std 的对应方法:

// src/log/log.go
var std = New(os.Stderr, "", LstdFlags)

func Printf(format string, v ...interface{}) {
std.Output(2, fmt.Sprintf(format, v...))
}

func Fatalf(format string, v ...interface{}) {
std.Output(2, fmt.Sprintf(format, v...))
os.Exit(1)
}

func Panicf(format string, v ...interface{}) {
s := fmt.Sprintf(format, v...)
std.Output(2, s)
panic(s)
}

自定义 Logger

如下定义自己的 Logger

package main

import (
"bytes"
"fmt"
"log"
)

type User struct {
Name string
Age int
}

func main() {
u := User{
Name: "dj",
Age: 18,
}

buf := &bytes.Buffer{}
logger := log.New(buf, "", log.Lshortfile|log.LstdFlags)

logger.Printf("%s login, age:%d", u.Name, u.Age)

fmt.Print(buf.String())
}

log.New 接受三个参数

  • io.Writer:日志都会写到这个 Writer 中;
  • prefix:前缀,也可以后面调用 logger.SetPrefix 设置;
  • flag:选项,也可以后面调用 logger.SetFlag 设置。

上面代码将日志输出到一个 bytes.Buffer,然后将这个 buf 打印到标准输出。可以注意到,第一个参数为 io.Writer,所以可以使用 io.MultiWriter 实现多目的地输出。下面我们将日志同时输出到 标准输出、 bytes.Buffer 和文件中:

package main

import (
"bytes"
"io"
"log"
"os"
)

type User struct {
Name string
Age int
}

func main() {
u := User{
Name: "dj",
Age: 18,
}

writer1 := &bytes.Buffer{}
writer2 := os.Stdout
writer3, err := os.OpenFile("log.txt", os.O_WRONLY|os.O_CREATE, 0755)
if err != nil {
log.Fatalf("create file log.txt failed: %v", err)
}

logger := log.New(io.MultiWriter(writer1, writer2, writer3), "", log.Lshortfile|log.LstdFlags)
logger.Printf("%s login, age:%d", u.Name, u.Age)
}

打印的颜色

如何设置打印的颜色呢?

package log

import (
"log"
"os"
)

var (
errorLog = log.New(os.Stdout, "\033[31m[error]\033[0m ", log.LstdFlags|log.Lshortfile)
infoLog = log.New(os.Stdout, "\033[34m[info ]\033[0m ", log.LstdFlags|log.Lshortfile)
)

// log methods
var (
Error = errorLog.Println
Errorf = errorLog.Printf
Info = infoLog.Println
Infof = infoLog.Printf
)

[info ] 颜色为蓝色,[error] 为红色。使用 log.Lshortfile 支持显示文件名和代码行号。

丢弃输出结果

可以通过 ioutil.Discard 来将打印结果丢弃

errorLog.SetOutput(ioutil.Discard)

编写文件日志模块

简单封装 log 库,使其支持简单的文件日志

新建 logging 目录,新建 file.golog.go 文件,写入内容:

设置日志文件

file.go:

package logging

import (
"fmt"
"log"
"os"
"time"
)

var (
LogSavePath = "runtime/logs/"
LogSaveName = "log"
LogFileExt = "log"
TimeFormat = "20060102"
)

func getLogFilePath() string {
return fmt.Sprintf("%s", LogSavePath)
}

func getLogFileFullPath() string {
prefixPath := getLogFilePath()
suffixPath := fmt.Sprintf("%s%s.%s", LogSaveName, time.Now().Format(TimeFormat), LogFileExt)

return fmt.Sprintf("%s%s", prefixPath, suffixPath)
}

func openLogFile(filePath string) *os.File {
_, err := os.Stat(filePath)
switch {
case os.IsNotExist(err):
mkDir()
case os.IsPermission(err):
log.Fatalf("Permission :%v", err)
}

handle, err := os.OpenFile(filePath, os.O_APPEND | os.O_CREATE | os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("Fail to OpenFile :%v", err)
}

return handle
}

func mkDir() {
// Getwd 返回与当前目录对应的根路径名
dir, _ := os.Getwd()
// MkdirAll 创建对应的目录以及所需的子目录,若成功则返回 nil,否则返回 error
err := os.MkdirAll(dir + "/" + getLogFilePath(), os.ModePerm)
if err != nil {
panic(err)
}
}

编写 Logger

log.go 文件

package logging

import (
"fmt"
"log"
"os"
"path/filepath"
"runtime"
)

type Level int

var (
F *os.File

DefaultPrefix = ""
DefaultCallerDepth = 2

logger *log.Logger
logPrefix = ""
levelFlags = []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL"}
)

const (
DEBUG Level = iota
INFO
WARNING
ERROR
FATAL
)

func init() {
filePath := getLogFileFullPath()
F = openLogFile(filePath)

logger = log.New(F, DefaultPrefix, log.LstdFlags)
}

func Debug(v ...interface{}) {
setPrefix(DEBUG)
logger.Println(v)
}

func Info(v ...interface{}) {
setPrefix(INFO)
logger.Println(v)
}

func Warn(v ...interface{}) {
setPrefix(WARNING)
logger.Println(v)
}

func Error(v ...interface{}) {
setPrefix(ERROR)
logger.Println(v)
}

func Fatal(v ...interface{}) {
setPrefix(FATAL)
logger.Fatalln(v)
}

func setPrefix(level Level) {
_, file, line, ok := runtime.Caller(DefaultCallerDepth)
if ok {
logPrefix = fmt.Sprintf("[%s][%s:%d]", levelFlags[level], filepath.Base(file), line)
} else {
logPrefix = fmt.Sprintf("[%s]", levelFlags[level])
}

logger.SetPrefix(logPrefix)
}

运行后,测试打印

logging.Info(err.Key, err.Message)

检查输出的文件:

[INFO][auth.go:45]2021/11/03 21:38:33 [Username.Required. Username Can not be empty]
[INFO][auth.go:45]2021/11/03 21:38:33 [Password.Required. Password Can not be empty]
[INFO][auth.go:45]2021/11/03 21:39:36 [Password.Required. Password Can not be empty]
[INFO][auth.go:45]2021/11/03 21:40:45 [Password.Required. Password Can not be empty]
[INFO][auth.go:45]2021/11/03 21:40:56 [Password.Required. Password Can not be empty]

错误日志

对于错误日志,它有 Fatal 和 Panic

  • Fatal 日志通过调用 os.Exit(1) 来结束程序
  • Panic 日志在写入日志消息之后抛出一个 panic

但是它缺少一个 ERROR 日志级别,这个级别可以在不抛出 panic 或退出程序的情况下记录错误

Reference